/* ************************************************************************ */
/*                                                                          */
/*  Copyright (c)2005 Russell Weir | damonsbane _at_ gmail                  */
/*                                                                          */
/* This library is free software; you can redistribute it and/or            */
/* modify it under the terms of the GNU Lesser General Public               */
/* License as published by the Free Software Foundation; either             */
/* version 3.0 of the License, or (at your option) any later version.       */
/*                                                                          */
/* This library is distributed in the hope that it will be useful,          */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of           */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU        */
/* Lesser General Public License or the LICENSE file for more details.      */
/*                                                                          */
/* ************************************************************************ */
package altdate;

// TODO: Internationalization
class Tools {
	public static var	JAN		: Int = 0;
	public static var	FEB		: Int = 1;
	public static var	MAR		: Int = 2;
	public static var	APR		: Int = 3;
	public static var	MAY		: Int = 4;
	public static var	JUN		: Int = 5;
	public static var	JUL		: Int = 6;
	public static var	AUG		: Int = 7;
	public static var	SEP		: Int = 8;
	public static var	OCT		: Int = 9;
	public static var	NOV		: Int = 10;
	public static var	DEC		: Int = 11;
	public static var	JANUARY		: Int = 0;
	public static var	FEBRUARY	: Int = 1;
	public static var	MARCH		: Int = 2;
	public static var	APRIL		: Int = 3;
	//public static var	MAY		: Int = 4;
	public static var	JUNE		: Int = 5;
	public static var	JULY		: Int = 6;
	public static var	AUGUST		: Int = 7;
	public static var	SEPTEMBER	: Int = 8;
	public static var	OCTOBER		: Int = 9;
	public static var	NOVEMBER	: Int = 10;
	public static var	DECEMBER	: Int = 11;

	//Monday, 15-Aug-05 15:52:01 UTC
	public static var	FMT_COOKIE	: String = "%A, %d-%b-%y %H:%M:%S %Z";
	//Mon, 15 Aug 05 15:52:01 +0000
	public static var	FMT_RFC_822	: String = "%a, %d %b %y %H:%M:%S %z";
	//Monday, 15-Aug-05 15:52:01 UTC usenet == rfc 850
	public static var	FMT_RFC_850	: String = "%A, %d-%b-%y %H:%M:%S %Z";
	public static var	FMT_USENET	: String = "%A, %d-%b-%y %H:%M:%S %Z";
	//Mon, 15 Aug 2005 15:52:01 +0000
	public static var	FMT_RFC_1123	: String = "%a, %d %b %Y %H:%M:%S %z";

	/**
		Parse English weekday name to integer (0=Sunday)
		Returns null if conversion fails.
	*/
	public static function dayToInt(s : String) : Null<Int> {
		switch(s.substr(0,3).toLowerCase()) {
		case "sun": return 0;
		case "mon": return 1;
		case "tue": return 2;
		case "wed": return 3;
		case "thu": return 4;
		case "fri": return 5;
		case "sat": return 6;
		}
		return null;
	}

	/**
		Format integer day (0=Sun) to English weekday name.
		shortform, if set to true, will return the 3 letter
		version.
		Returns empty string if conversion fails.
	*/
	public static function dayToString(day:Int,?shortform:Bool) : String
	{
		switch(day) {
		case 0: if(shortform) return "Sun"; else return "Sunday";
		case 1: if(shortform) return "Mon"; else return "Monday";
		case 2: if(shortform) return "Tue"; else return "Tuesday";
		case 3: if(shortform) return "Wed"; else return "Wednesday";
		case 4: if(shortform) return "Thu"; else return "Thursday";
		case 5: if(shortform) return "Fri"; else return "Friday";
		case 6: if(shortform) return "Sat"; else return "Saturday";
		}
		return "";
	}

	/**
		Take the fractional part of a decimal day and convert
		it to a number of seconds. 1.5 is 1 and 1/2 day, which
		is 12 hours, or 43,200 seconds. Always returns the
		absolute value, so -1.5 still returns 43,200.
	*/
	public static function decimalDayTimeToSeconds(day:Float) : Int {
		day = Math.abs(day);
		//var totalSeconds : Int = int((day - Math.floor(day)) * 86400);
		// minimize floating point error.
		return Math.round(
			(day * 86400) -
			(Math.floor(day) * 86400)
		);
	}

	/**
		Better floating point to string
	*/
	public static function floatToString(f:Float) : String {
		var sb = new StringBuf();
		var i : Int = int(f);
		sb.add(i);
		sb.add(".");
		f = Math.abs(f);
		var v : String = Std.string(f - Math.abs(i));
		sb.add(v);
		var s = StringTools.replace(sb.toString(), ".0.",".");
		return s;
	}

	/**
		Since Math.floor and Std.int both return the same
		values, this is an actual (int)Float cast.
	*/
	public static function int(f:Float) : Int {
		if(f < 0)
			return Math.ceil(f);
		return Math.floor(f);
	}

	/**
		Parse English month name to integer (0=January)
		Returns null if conversion fails.
	*/
	public static function monthToInt(s : String) : Null<Int> {
		switch(s.substr(0,3).toLowerCase()) {
		case "jan": return 0;
		case "feb": return 1;
		case "mar": return 2;
		case "apr": return 3;
		case "may": return 4;
		case "jun": return 5;
		case "jul": return 6;
		case "aug": return 7;
		case "sep": return 8;
		case "oct": return 9;
		case "nov": return 10;
		case "dec": return 11;
		}
		return null;
        }

	/**
		Format integer month (0=January) to English month name.
		shortform, if set to true, will return the 3 letter
		version.
		Returns empty string if conversion fails.
	*/
	public static function monthToString(m:Int,?shortform:Bool) : String {
		switch(m) {
		case 0: if(shortform) return "Jan"; else return "January";
		case 1: if(shortform) return "Feb"; else return "February";
		case 2: if(shortform) return "Mar"; else return "March";
		case 3: if(shortform) return "Apr"; else return "April";
		case 4: if(shortform) return "May"; else return "May";
		case 5: if(shortform) return "Jun"; else return "June";
		case 6: if(shortform) return "Jul"; else return "July";
		case 7: if(shortform) return "Aug"; else return "August";
		case 8: if(shortform) return "Sep"; else return "September";
		case 9: if(shortform) return "Oct"; else return "October";
		case 10: if(shortform) return "Nov"; else return "November";
		case 11: if(shortform) return "Dec"; else return "December";
		}
		return "";
        }

	/**
		Parse [-]HH:MM:SS or HH:MM:SS (am|pm) to an integer
		number of seconds. Returns null if time could not
		be parsed.
	*/
	// TODO: returns 0 if time not parsed in Neko
	// check Std.hx parseInt()
	public static function timeToSeconds(s : String) : Null<Int> {
		var h : Null<Int> = 0;
		var m : Null<Int> = 0;
		var sec : Null<Int> = 0;
		var pm : Bool = false;
		var ampm : Bool = false;

		var t = s.split(":");
		var extra = t[t.length-1].split(" ");
		if(extra.length > 0) {
			t[t.length-1] = extra[0];
			if(extra.length > 1) {
				switch(extra[1].toLowerCase()) {
				case "am":
					ampm = true;
				case "pm":
					pm = true;
					ampm = true;
				}
			}
		}

#if neko
		var ereg=~/^([\s]*)-?([0-9]+)/;
		if(!ereg.match(t[0]))
			return null;
#end
		h = Std.parseInt(t[0]);
		if(h == null)
			return null;
		if(ampm && h > 12) {
			if(h > 12)
				return null;
			if(pm)
				h += 12;
		}

		if(t.length > 1) {
#if neko
			if(!ereg.match(t[1]))
				return null;
#end
			m = Std.parseInt(t[1]);
			if(m == null)
				return null;
			if(t.length > 2) {
#if neko
				if(!ereg.match(t[1]))
					return null;
#end
				sec = Std.parseInt(t[2]);
				if(sec == null)
					return null;
			}
		}

		return (h * 3600) + (m * 60) + sec;
	}

	/**
		Convert an integer number of seconds into a decimal
		day. 1 second = 0.00001157407407, 1 day = 1.0
	*/
	public static function secondsToDecimalDay(sec:Int) : Float {
		return sec/86400;
	}

	/**
		Change a number of seconds into [-]HH:MM:SS
	*/
	public static function secondsToTime(value:Int) : String {
		var sb = new StringBuf();
		if(value < 0)
			sb.add('-');
		var v : Int = int(Math.abs(value));
		var s :Int = Math.floor(v/3600);
		if(s < 10) sb.add("0");
		sb.add(s);
		sb.add(':');
		v = v % 3600;
		s = Math.floor(v/60);
		if(s < 10) sb.add("0");
		sb.add(s);
		sb.add(':');
		s = v % 60;
		if(s < 10) sb.add("0");
		sb.add(s);
		return sb.toString();
	}

	/**
		Format a date like strftime or DateTools.format in haxe.
		Warning, some % tags are not supported yet, your mileage
		may vary.
	*/
	// TODO: Internationalization
	public static function format(gd:Gregorian, fmt:String) : String {
		/*
		We can't convert from haxe Date. See Gregorian.hx
		var f = gd.getTime();
		// use native lib, if it can
		if(f != null) {
			return DateTools.format(Date.fromTime(f), fmt);
		}
		*/
		var sb = new StringBuf();

		var calcSeconds = function() : String {
			var seconds : Int = gd.offset + gd.save;
			var s = new StringBuf();
			if(seconds < 0) {
				s.add("-");
				seconds = Math.floor(Math.abs(seconds));
			}
			s.add(zeroPadDateNum(Math.floor(seconds / 3600)));
			s.add(zeroPadDateNum(Math.floor(seconds % 3600)));
			return s.toString();
		}

		var x : Int = 0;
		while(x < fmt.length) {
			var c :String = fmt.charAt(x);
			if(c != '%' || x == fmt.length - 1) {
				sb.add(c);
				x++;
				continue;
			}
			switch(fmt.charAt(x+1)) {
			default:
				x -= 1;
				sb.add('%');
			case "a": sb.add(dayToString(gd.dayOfWeek(),true));
			case "A": sb.add(dayToString(gd.dayOfWeek(),false));
			case "b": sb.add(monthToString(gd.month,true));
			case "B": sb.add(monthToString(gd.month,false));
			case "C": sb.add(Std.string(Math.floor(gd.year/100)));
			case "d": sb.add(zeroPadDateNum(gd.day));
			case "D": //"%m/%d/%y"
				sb.add(zeroPadDateNum(gd.month + 1));
				sb.add("/");
				sb.add(zeroPadDateNum(gd.day));
				sb.add("/");
				sb.add(zeroPadDateNum(gd.year % 100));
			case "e": sb.add(zeroPadDateNum(gd.day, true));
			case "E": sb.add("%E"); // unsupported
			case "F": // "%Y-%m-%d"
				sb.add(Std.string(gd.year));
				sb.add("-");
				sb.add(zeroPadDateNum(gd.month + 1));
				sb.add("-");
				sb.add(zeroPadDateNum(gd.day));
			case "G": sb.add("%G"); //unsupported
			case "g": sb.add("%g"); //unsupported
			case "h": sb.add(monthToString(gd.month,true));
			case "H": sb.add(zeroPadDateNum(gd.hours));
			case "I":
				// v code fails on flash9 v
				//sb.add(zeroPadDateNum(if(gd.hours>12) gd.hours-12; else gd.hours));
				if(gd.hours > 12)
					sb.add(zeroPadDateNum(gd.hours-12));
				else
					sb.add(zeroPadDateNum(gd.hours));
			case "j": sb.add("%j"); //unsupported
			case "k": sb.add(zeroPadDateNum(gd.hours,true));
			case "l":
				// v code fails on flash9 v
				//sb.add(zeroPadDateNum(if(gd.hours>12) gd.hours-12; else gd.hours,true));
				if(gd.hours > 12)
					sb.add(zeroPadDateNum(gd.hours-12,true));
				else
					sb.add(zeroPadDateNum(gd.hours,true));
			case "m": sb.add(zeroPadDateNum(gd.month + 1));
			case "M": sb.add(zeroPadDateNum(gd.minutes));
			case "n": sb.add("\n");
			case "O": sb.add("%O"); // unsupported
			case "p": sb.add(if(gd.hours<12) "AM"; else "PM");
			case "P": sb.add(if(gd.hours<12) "am"; else "pm");
			case "r": //"%I:%M:%S %p"
				// v code fails on flash9 v
				//sb.add(zeroPadDateNum(if(gd.hours>12) gd.hours-12; else gd.hours));
				if(gd.hours > 12)
					sb.add(zeroPadDateNum(gd.hours-12));
				else
					sb.add(zeroPadDateNum(gd.hours));
				sb.add(":");
				sb.add(zeroPadDateNum(gd.minutes));
				sb.add(":");
				sb.add(zeroPadDateNum(gd.seconds));
				sb.add(" ");
				sb.add(if(gd.hours<12) "AM"; else "PM");
			case "R": //"%H:%M"
				sb.add(zeroPadDateNum(gd.hours));
				sb.add(":");
				sb.add(zeroPadDateNum(gd.minutes));
			case "s": sb.add(Std.string(Math.floor(gd.getTime())));
			case "S": sb.add(zeroPadDateNum(gd.seconds)); //TODO: leap seconds
			case "t": sb.add("\t");
			case "T": //"%H:%M:%S"
				sb.add(zeroPadDateNum(gd.hours));
				sb.add(":");
				sb.add(zeroPadDateNum(gd.minutes));
				sb.add(":");
				sb.add(zeroPadDateNum(gd.seconds));
			case "u": sb.add(if(gd.dayOfWeek()>0) Std.string(gd.dayOfWeek()); else "7");
			case "U": sb.add("%U"); // unsupported
			case "V": sb.add("%V"); // unsupported
			case "w": sb.add(Std.string(gd.dayOfWeek()));
			case "W": sb.add("%W"); // unsupported
			case "x": sb.add("%x"); // unsupported
			case "X": sb.add("%X"); // unsupported
			case "y": sb.add(zeroPadDateNum(gd.year % 100));
			case "Y": sb.add(Std.string(gd.year));
			case "z": sb.add(calcSeconds());
			case "Z": sb.add(gd.tzabbrev);
			case "+": sb.add("%+"); // unsupported
			case "%": sb.add("%");
			}
			x += 2;
		}
		return sb.toString();
	}

	/**
		Return a string with a leading 0 for any values
		less than 10. If spaceInstead is true, a space
		will be used instead of a zero
	*/
	public static function zeroPadDateNum(n:Int, ?spaceInstead:Bool) : String {
		var sb = new StringBuf();
		if(n < 10) {
			if(spaceInstead == true)
				sb.add(" ");
			else
				sb.add("0");
		}
		sb.add(Std.string(n));
		return sb.toString();
	}
}